<!DOCTYPE html>
<html lang="en">
  <head>
    <!-- 设置字符编码为 UTF-8 -->
    <meta charset="UTF-8" />
    <!-- 设置视口，确保页面在不同设备上正确显示 -->
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <!-- 设置页面标题 -->
    <title>Document</title>
  </head>
  <style>
    /* 列表容器样式 */
    .list-container {
      /* 当内容溢出时显示滚动条 */
      overflow: auto;
      /* 容器高度 */
      height: 600px;
      /* 容器宽度 */
      width: 600px;
      /* 容器上下外边距为 100px，左右自动居中 */
      margin: 100px auto;
      /* 容器边框 */
      border: 1px solid black;
      /* 容器高度 */
      height: 500px;
    }
    /* 列表样式 */
    .list-container-list {
      /* 文本居中对齐 */
      text-align: center;
    }
  </style>
  <body>
    <!-- 列表容器 -->
    <div class="list-container">
      <!-- 列表 -->
      <div class="list-container-list"></div>
    </div>

    <script>
      // 虚拟列表类
      class VirtualList {
        // 构造函数，接收容器选择器、列表选择器和每个列表项的初始高度作为参数
        constructor(containerSelector, list, itemHeight) {
          // 每个列表项的初始高度
          this.itemHeight = itemHeight
          // 容器的高度
          this.containerHeight = 0
          // 缓存每个列表项的实际高度
          this.cacheHeightMap = {}
          // 存储列表数据
          this.data = []
          // 获取列表容器元素
          this.container = document.querySelector(containerSelector)
          // 获取列表元素
          this.list = document.querySelector(list)
          // 初始化虚拟列表
          this.init()
        }

        // 初始化方法
        init() {
          // 获取容器的实际高度
          this.containerHeight = this.container.offsetHeight
          // 初始化列表数据
          this.initData()
          // 处理滚动回调
          this.scrollCallback()
          // 绑定滚动事件
          this.bindEvent()
        }
        // 绑定滚动事件方法
        bindEvent() {
          // 为容器添加滚动事件监听器，使用节流函数处理滚动回调
          this.container.addEventListener(
            'scroll',
            this.throttle(this.scrollCallback.bind(this))
          )
        }
        // 初始化数据方法
        initData() {
          // 生成 15 条列表数据
          for (let i = 0; i < 15; i++) {
            // 随机生成列表项的高度，范围在 60 - 150 之间
            const height = Math.floor(
              //60 -150的高度
              Math.random() * (150 - this.itemHeight + 1) + this.itemHeight
            )
            // 将数据添加到 data 数组中
            this.data.push({
              // 列表项内容
              content: `内容:${this.data.length}`,
              // 列表项高度
              height,
              // 列表项背景颜色，根据索引奇偶性设置
              color: i % 2 ? 'red' : 'yellow',
            })
          }
        }

        // 滚动回调方法
        scrollCallback() {
          // 列表内容总高度
          let contentHeight = 0
          // 列表的偏移量
          let transformY = 0
          // 开始渲染的列表项索引
          let startIndex
          // 结束渲染的列表项索引
          let endIndex
          // 获取容器的滚动距离
          const scrollTop = this.container.scrollTop

          // 遍历数据数组，计算开始和结束索引
          for (let i = 0; i < this.data.length; i++) {
            // 获取缓存的列表项高度，如果没有缓存则使用初始高度
            const usedHeight = this.cacheHeightMap[i]
              ? this.cacheHeightMap[i]
              : this.itemHeight

            // 累加列表项高度
            contentHeight += usedHeight

            // 当累加高度超过滚动距离时，确定开始索引
            if (contentHeight >= scrollTop && startIndex === undefined) {
              startIndex = i
              // 计算列表的偏移量
              transformY = contentHeight - usedHeight
            }

            // 当累加高度超过滚动距离加上容器高度时，确定结束索引
            if (
              contentHeight > scrollTop + this.containerHeight &&
              endIndex === undefined
            ) {
              endIndex = i
            }
          }

          // 如果结束索引未确定，继续初始化数据
          if (endIndex === undefined) {
            this.initData()
            endIndex = this.data.length - 1
          }

          // 截取需要渲染的数据
          const renderList = this.data.slice(startIndex, endIndex + 1)

          // 生成 HTML 字符串
          const component = renderList
            .map((itemData) => {
              return `<div style="height: ${itemData.height}px; background: ${itemData.color};">${itemData.content}</div>`
            })
            .join('')
          // 将 HTML 字符串插入到列表中
          this.list.innerHTML = component
          // 设置列表的偏移量
          this.list.style.transform = `translate(0,${transformY}px)`
          // 设置列表的高度
          this.list.style.height = `${contentHeight - transformY}px`

          // 获取列表的子元素
          const children = this.list.children
          let flag = startIndex
          // 遍历子元素，缓存实际高度
          for (const child of children) {
            this.cacheHeightMap[flag] = child.offsetHeight
            flag++
          }
        }

        // 节流函数
        throttle(fn) {
          // 节流开关
          let enabled = true
          return function () {
            if (enabled) {
              // 关闭节流开关
              enabled = false
              // 使用 requestAnimationFrame 优化性能
              window.requestAnimationFrame(fn)
              // 打开节流开关
              enabled = true
            }
          }
        }
      }

      // 初始化VirtualList实例
      const virtualList = new VirtualList(
        '.list-container',
        '.list-container-list',
        60
      )
    </script>
  </body>
</html>
